﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Text;
using Crm2011.Framework.Core;



namespace ScriptEase
{
    /// <summary>
    /// The Workspace maintains the <see cref="MappingList"/>
    /// and it provides the functionality to update,
    /// merge and observe files in a local Workspace.
    /// </summary>
    public class Workspace : IDisposable
    {
        //private WorkspaceNotification _workspaceNotification;
        private readonly List<IWorkspaceMapper> _mappers;
        private readonly bool _readonlyManaged;
        private FileObserver _fileObserver;



        /// <summary>
        /// Create a new Workspace instance.
        /// </summary>
        /// <param name="projectDirectory">The project directory path.</param>
        /// <param name="webResourceBaseDirectory">The base directory as configured in the project configuration.
        ///     Can be relative to the <paramref name="projectDirectory" /> or absolute.</param>
        /// <param name="mappedFileChanged">The local file changed.</param>
        /// <param name="mappers">A list of <see cref="IWorkspaceMapper" />.</param>
        /// <param name="readonlyManaged"></param>
        /// <exception cref="System.ArgumentNullException">webResourceBaseDirectory;@The web-resources base directory must not be null!</exception>
        public Workspace(string projectDirectory, string webResourceBaseDirectory, FileObserver.FileChangedHandler mappedFileChanged,
            List<IWorkspaceMapper> mappers, bool readonlyManaged)
        {
            if (String.IsNullOrWhiteSpace(webResourceBaseDirectory)) webResourceBaseDirectory = ".";

            Trace.Default.Info("Initializing project workspace! Web-Resource base directory='{0}'", webResourceBaseDirectory);
            if (!Path.IsPathRooted(webResourceBaseDirectory))
            {
                // The base directory is relative to 'projectFilePath'.
                webResourceBaseDirectory = Path.GetFullPath(Path.Combine(projectDirectory, webResourceBaseDirectory));
            }

            string p = Environment.ExpandEnvironmentVariables(webResourceBaseDirectory);
            this.WebResourcesBasePath = Path.GetFullPath(p);
            Trace.Default.Debug("Web-Resource full path='{0}'", this.WebResourcesBasePath);

            if (!Directory.Exists(this.WebResourcesBasePath))
            {
                Trace.Default.Warn("Directory {0} does not exist --> creating...", this.WebResourcesBasePath);
                Directory.CreateDirectory(this.WebResourcesBasePath);
            }

            foreach (IWorkspaceMapper workspaceMapper in mappers)
            {
                workspaceMapper.Initialize(this);
            }
            _mappers = mappers;
            _readonlyManaged = readonlyManaged;

            //
            // Setup file observation and register a handler for file notifications (fileChanged)
            // The file observation works automatically. Means whenever the MappingList or and 
            // item within change (ref. mappingsCollectionChanged) 
            // the FileObserver subscription will be updated automatically.
            //
            _fileObserver = new FileObserver();
            _fileObserver.Subscribe(fileChanged); // first register the local handler (to be able to cancel the event chain)
            _fileObserver.Subscribe(mappedFileChanged); // also register the project's handler

            this.Mappings = new MappingList();
            this.Mappings.CollectionChanged += mappingsCollectionChanged;
        }



        private void disposeManaged()
        {
            if (_fileObserver != null)
            {
                _fileObserver.Dispose();
                _fileObserver = null;
            }
            if (this.Mappings != null)
            {
                this.Mappings.CollectionChanged -= mappingsCollectionChanged;
            }
        }



        public MappingList Mappings { get; private set; }


        /// <summary>
        /// Gets the full path of the local directory where the WebResources are stored.
        /// </summary>
        public string WebResourcesBasePath { get; private set; }


        #region File and Directory Conversions


        /// <summary>
        /// Gets the full path of a file or directory.
        /// </summary>
        /// <returns>The full path of a file or directory which can be relative to <paramref name="relativeRootPath"/>
        /// or <c>null</c> if the full path or directory does not exist.</returns>
        public static string GetFullPathExists(string fileOrDirectory, string relativeRootPath)
        {
            if (fileOrDirectory == null) return null;
            string fullPath = GetFullPath(fileOrDirectory, relativeRootPath);
            return File.Exists(fullPath) ? fullPath : null;
        }



        /// <summary>
        /// Gets the full path of a file or directory.
        /// </summary>
        /// <returns><paramref name="fileOrDirectory"/> if this path is rooted,
        ///  otherwise the combined path of <paramref name="relativeRootPath"/> and <paramref name="fileOrDirectory"/>
        /// </returns>
        public static string GetFullPath(string fileOrDirectory, string relativeRootPath)
        {
            if (fileOrDirectory == null) return null;
            return Path.IsPathRooted(fileOrDirectory)
                ? fileOrDirectory
                : Path.GetFullPath(Path.Combine(relativeRootPath, fileOrDirectory));
        }



        /// <summary>
        /// Gets the relative path of a file or directory to a given <paramref name="relativeRootPath"/>.
        /// </summary>
        /// <remarks>If <paramref name="fileOrDirectory"/> is rooted but it is not a 
        /// child of <paramref name="relativeRootPath"/> the full path is returned 
        /// because a relative path could not be computed.</remarks>
        private string getRelativePath(string fileOrDirectory, string relativeRootPath)
        {
            if (!Path.IsPathRooted(fileOrDirectory)) return fileOrDirectory; // already relative

            // fileOrDirectory is an absolute path
            return fileOrDirectory.StartsWith(relativeRootPath, StringComparison.InvariantCultureIgnoreCase)
                ? fileOrDirectory.Remove(0, relativeRootPath.Length + 1)
                : fileOrDirectory;
        }


        #endregion


        public void InitializeMap(List<WebResource> webResources)
        {
            this.Mappings.BeginUpdate();
            createMap(webResources);

            //
            // compare files in the local Workspace with the current WebResource content from CRM
            //
            foreach (WebResource webResource in webResources)
            {
                MappingModel mapping = this.Mappings.GetMapping(webResource);
                if (mapping.FileInfo == null) continue; // no local file

                // there is a file in the local Workspace, compare with the CRM WebResource
                if (this.compare(mapping.FileInfo.FullName, webResource))
                {
                    Trace.Default.Debug("Web resource {0} content and local file match!", webResource.Name);
                    mapping.SyncStatus = FileSyncStatus.Published;
                }
                else
                {
                    // CRM and local File are different

                    // Not customizable files are rad/only and managed files can be read/only
                    if ((mapping.IsManaged && _readonlyManaged) || !mapping.IsCustomizable)
                    {
                        this.UpdateLocalFile(webResource, ConflictResolver.OverrideLocal);
                        // this will result in a pending update, cause ScriptEase will recognize a file change!
                    }
                    else
                    {
                        mapping.SyncStatus = ConflictResolver.CreateMergeFile(mapping.FileInfo.FullName, webResource);
                    }
                }
            }
            this.Mappings.EndUpdate(); // --> raises collection changed event
        }



        public void CloseMap()
        {
            this.Mappings.Clear(); // --> raises collection changed event
        }



        /// <summary>
        /// The MappingList has changed. Update the file observations accordingly.
        /// </summary>
        private void mappingsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    // Register file notifications for all new items that have a FileInfo
                    if (e.NewItems != null)
                        foreach (MappingModel mapping in e.NewItems.Cast<MappingModel>().Where(m => m.FileInfo != null))
                        {
                            _fileObserver.RegisterFile(mapping.FileInfo.FullName);
                        }
                    break;

                case NotifyCollectionChangedAction.Remove:
                    if (e.OldItems != null)
                        foreach (var oldItem in e.OldItems.Cast<MappingModel>().Where(m => m.FileInfo != null))
                        {
                            _fileObserver.UnRegisterFile(oldItem.FileInfo.FullName);
                        }
                    break;

                case NotifyCollectionChangedAction.Reset:
                    if (_fileObserver != null) _fileObserver.UnRegisterAll();
                    break;

                    //case NotifyCollectionChangedAction.Replace:
                    //case NotifyCollectionChangedAction.Move:
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }



        private void fileChanged(object sender, FileChangedEventArgs e)
        {
            MappingModel mapping = this.Mappings.GetMapping(e.FileInfo.FullName);
            if (mapping == null)
            {
                e.Cancel = true;
            }
            else
            {
                //
                // NOTE: The mappingModel.FileInfo contains the old (previous) file info
                //       where e.FileInfo contains the information about the current file.
                //
                Trace.Default.Info("EVENT: {0} --> {1}", mapping.FileInfo.FullName, mapping.WebResourceName);
                if (e.FileInfo.Length == mapping.FileInfo.Length && e.FileInfo.LastWriteTimeUtc == mapping.FileInfo.LastWriteTimeUtc)
                {
                    Trace.Default.Debug("Received file changed event for file {0} but no observed property changed!??", e.FileInfo.Name);
                    e.Cancel = true;
                }
                else
                {
                    mapping.FileInfo = e.FileInfo;

                    Trace.Default.Debug("Mapped file changed {0}, ignore={1}", mapping.FileRelativePath, mapping.IgnoreNextFileChange);
                    if (mapping.IgnoreNextFileChange)
                    {
                        e.Cancel = true;
                        mapping.IgnoreNextFileChange = false;
                    }
                }
            }
        }



        public void Filter(List<SolutionComponent> webResourceComponents)
        {
            try
            {
                this.Mappings.BeginUpdate();
                foreach (MappingModel mapping in this.Mappings)
                {
                    bool isInSelectedSolution = webResourceComponents.Any(sc => sc.ObjectId == mapping.WebResourceId);
                    mapping.AutoUpdate = isInSelectedSolution &&
                                         ((mapping.IsCustomizable && !mapping.IsManaged) ||  // unmanaged items 
                                          (mapping.IsManaged && !_readonlyManaged));    // or managed nut these are not readonly
                }
            }
            catch (Exception e)
            {
                Trace.Default.LogException(e);
            }
            finally
            {
                this.Mappings.EndUpdate();
            }
        }



        /// <summary>
        /// Update a file in the local Workspace from a WebResource.
        /// </summary>
        /// <param name="webResource">The latest web resource.</param>
        /// <param name="cr">A conflict resolution handler that is used if the file already
        /// exists in the local workspace..</param>
        public void UpdateLocalFile(WebResource webResource, ConflictResolver.ResolutionHandler cr)
        {
            MappingModel mapping = this.Mappings.GetMapping(webResource);

            string fullPath = GetFullPath(mapping.FileRelativePath, this.WebResourcesBasePath);
            if (mapping.FileInfo == null)
            {
                // File does not yet exist in the local workspace
                ConflictResolver.OverrideLocal(fullPath, webResource);
                mapping.FileInfo = new FileInfo(fullPath);
                mapping.FileRelativePath = getRelativePath(fullPath, this.WebResourcesBasePath);
                mapping.SyncStatus = FileSyncStatus.Published;
                _fileObserver.RegisterFile(fullPath);
            }

            else if (!this.compare(fullPath, webResource))
            {
                // a local file already exists and it has a different content 
                // than the WebResource that should be saved.
                mapping.IgnoreNextFileChange = true;
                mapping.SyncStatus = cr(fullPath, webResource);
                // basically not ok but if the file was not written to disc we do not expect an file change event 
                if (mapping.SyncStatus != FileSyncStatus.Published) mapping.IgnoreNextFileChange = false;
            }
        }



        /// <summary>
        /// Gets the WebResource content of a file from the local workspace.
        /// </summary>
        /// <returns>The file's content or <c>null</c> if the file does not exist.</returns>
        public string GetFile(MappingModel mapping)
        {
            string fullName = GetFullPathExists(mapping.FileRelativePath, this.WebResourcesBasePath);
            return fullName == null ? null : FileEncoding.GetEncodedFileContents(fullName);
        }



        private bool compare(string filePath, WebResource webResource)
        {
            string webResourceContent = (webResource.Content != null) ? FileEncoding.DecodeFrom64(webResource.Content) : String.Empty;
            string localFileContent = File.ReadAllText(filePath, Encoding.UTF8);
            return webResourceContent == localFileContent;
        }



        /// <summary>
        /// Builds the mapping list for the given web-resources.
        /// </summary>
        private void createMap(IEnumerable<WebResource> webResources)
        {
            Trace.Default.Debug("Build the mapping list for the given web-resources.");

            foreach (WebResource webResource in webResources)
            {
                try
                {
                    foreach (IWorkspaceMapper workspaceMapper in _mappers)
                    {
                        string localFilePath = workspaceMapper.GetFilePath(webResource);
                        if (localFilePath == null) continue;

                        this.Mappings.Add(new MappingModel(webResource, localFilePath, this.getRelativePath(localFilePath, this.WebResourcesBasePath)));
                        break;
                    }
                }
                catch (Exception e)
                {
                    Trace.Default.LogException(e);
                }
            }
            Trace.Default.Info("{0} WebResources to file mappings", this.Mappings.Count);
        }


        #region IDisposable


        public void Dispose()
        {
            dispose(true);
            GC.SuppressFinalize(this);
        }



        private void dispose(bool disposing)
        {
            if (!this._disposed)
            {
                if (disposing)
                    disposeManaged();
                //CloseHandle(handle);
            }
            _disposed = true;
        }



        private bool _disposed;


        #endregion
    }
}